{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "naMQPO5iRpNE"
   },
   "source": [
    "#### Copyright 2018 Google LLC。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "sHpbnneJYcwJ"
   },
   "outputs": [],
   "source": [
    "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#\n",
    "# https://www.apache.org/licenses/LICENSE-2.0\n",
    "#\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "84x4Fxc5lzFv"
   },
   "source": [
    "# 机器学习公平性简介\n",
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "J8daw3YOIAXH"
   },
   "source": [
    "## 免责声明\n",
    "本练习仅探讨与机器学习公平性相关的一小部分概念和技术；内容并不全面！"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "xFxZOg55lWJE"
   },
   "source": [
    "## 学习目标\n",
    "\n",
    "* 提高对模型数据中可能出现的不同类型偏差的认识。\n",
    "* 在训练模型之前先探索特征数据，以主动识别潜在的偏差来源\n",
    "* 按子群组评估模型表现，而不是评估总体表现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "l-K-xqksm-X3"
   },
   "source": [
    "## 概述\n",
    "\n",
    "在本练习中，您将探索数据集、围绕*公平性*评估分类器，并注意不良偏差会以何种方式影响机器学习 (ML)。\n",
    "\n",
    "在整个过程中，您将看到**公平意识**任务，这些任务使您有机会将机器学习过程与公平性联系起来。在执行这些任务时，您将识别偏差，并思考不解决这些偏差对模型预测的长期影响。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "TXkkHYyJ98_k"
   },
   "source": [
    "## 数据集和预测任务简介\n",
    "\n",
    "在本练习中，您将使用[成年人普查收入数据集](https://archive.ics.uci.edu/ml/datasets/Census+Income)，这是机器学习文献中常用的数据集。该数据由 Ronny Kohavi 和 Barry Becker 从 [1994 年人口普查局数据库](http://www.census.gov/en.html)中提取而来。\n",
    "\n",
    "数据集中的每个样本都包含参与 1994 年人口普查的一组个体的下列人口统计数据：\n",
    "\n",
    "### 数值特征\n",
    "*   `age`：个体的年龄。\n",
    "*   `fnlwgt`：人口普查机构认为这组观察数据代表的个体人数。\n",
    "*   `education_num`：教育类别表示法的列举。数值越大，个体的教育水平就越高。例如，`education_num` 为 `11` 表示 `Assoc_voc`（职业学校的副学士学位）、`education_num` 为 `13` 表示 `Bachelors`（学士学位）、`education_num` 为 `9` 表示 `HS-grad`（高中毕业）。\n",
    "*   `capital_gain`：个体的资本收益，以美元表示。\n",
    "*   `capital_loss`：个体的资本损失，以美元表示。\n",
    "*   `hours_per_week`：每周工作时数。\n",
    "\n",
    "### 类别特征\n",
    "*   `workclass`：个体的雇主类型。例如：`Private`、`Self-emp-not-inc`、`Self-emp-inc`、`Federal-gov`、`Local-gov`、`State-gov`、`Without-pay` 和 `Never-worked`。\n",
    "*   `education`：个体取得的最高教育水平。\n",
    "*   `marital_status`：个体的婚姻状况。例如：Married-civ-spouse`、`Divorced`、`Never-married`、`Separated`、`Widowed`、`Married-spouse-absent` 和 `Married-AF-spouse`。\n",
    "*   `occupation`：个体的职业。例如：`tech-support`、`Craft-repair`、`Other-service`、`Sales`、`Exec-managerial` 等等。\n",
    "*   `relationship`：每个人在家庭关系中的角色。例如：`Wife`、`Own-child`、`Husband`、`Not-in-family`、`Other-relative` 和 `Unmarried`。\n",
    "*   `gender`：个体的性别，只能有两种选择：`Female` 或 `Male`。\n",
    "*   `race`：`White`、`Asian-Pac-Islander`、`Amer-Indian-Eskimo`、`Black` 和 `Other`。\n",
    "*   `native_country`：个体的原籍国。例如：`United-States`、`Cambodia`、`England`、`Puerto-Rico`、`Canada`、`Germany`、`Outlying-US(Guam-USVI-etc)`、`India`、`Japan`、`United-States`、`Cambodia`、`England`、`Puerto-Rico`、`Canada`、`Germany`、`Outlying-US(Guam-USVI-etc)`、`India`、`Japan` 等等。\n",
    "\n",
    "### 预测任务\n",
    "预测任务是确定一个人的年收入是否超过 5 万美元。**\n",
    "\n",
    "### 标签\n",
    "*   `income_bracket`：此人的年收入是否超过 5 万美元。\n",
    "\n",
    "### 关于数据集的注意事项\n",
    "\n",
    "为此数据集提取的所有样本都满足以下条件：\n",
    "*   `age` 为 16 岁或以上。\n",
    "*   调整后的总收入（用于计算 `income_bracket`）每年超过 100 美元。\n",
    "*   `fnlwgt` 大于 0。\n",
    "*   `hours_per_week` 大于 0。\n",
    "\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "I0RMIktKy8xX"
   },
   "source": [
    "## 设置\n",
    "\n",
    "首先，导入一些将在整个笔记本中使用的模块。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "2e_0DJJ8zE29"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import pandas as pd\n",
    "import tensorflow as tf\n",
    "import tempfile\n",
    "!pip install seaborn==0.8.1\n",
    "import seaborn as sns\n",
    "import itertools\n",
    "from sklearn.metrics import confusion_matrix\n",
    "from sklearn.metrics import roc_curve, roc_auc_score\n",
    "from sklearn.metrics import precision_recall_curve\n",
    "from google.colab import widgets\n",
    "# For facets\n",
    "from IPython.core.display import display, HTML\n",
    "import base64\n",
    "!pip install facets-overview==1.0.0\n",
    "from facets_overview.feature_statistics_generator import FeatureStatisticsGenerator\n",
    "\n",
    "print('Modules are imported.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "-xgIRapb5LaQ"
   },
   "source": [
    "### 加载成年人数据集\n",
    "\n",
    "导入模块后，现在我们可以将成年人数据集加载到 Pandas DataFrame 数据结构中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "TeCNVvVUVS0P"
   },
   "outputs": [],
   "source": [
    "COLUMNS = [\"age\", \"workclass\", \"fnlwgt\", \"education\", \"education_num\",\n",
    "           \"marital_status\", \"occupation\", \"relationship\", \"race\", \"gender\",\n",
    "           \"capital_gain\", \"capital_loss\", \"hours_per_week\", \"native_country\",\n",
    "           \"income_bracket\"]\n",
    "\n",
    "train_df = pd.read_csv(\n",
    "    \"https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data\",\n",
    "    names=COLUMNS,\n",
    "    sep=r'\\s*,\\s*',\n",
    "    engine='python',\n",
    "    na_values=\"?\")\n",
    "test_df = pd.read_csv(\n",
    "    \"https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test\",\n",
    "    names=COLUMNS,\n",
    "    sep=r'\\s*,\\s*',\n",
    "    skiprows=[0],\n",
    "    engine='python',\n",
    "    na_values=\"?\")\n",
    "\n",
    "# Drop rows with missing values\n",
    "train_df = train_df.dropna(how=\"any\", axis=0)\n",
    "test_df = test_df.dropna(how=\"any\", axis=0)\n",
    "\n",
    "print('UCI Adult Census Income dataset loaded.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "coilRN-hooja"
   },
   "source": [
    "## 用 Facets 分析成年人数据集\n",
    "\n",
    "正如在 MLCC 中提到的，在直接进行预测*之前*，了解数据集很重要。\n",
    "\n",
    "在检查数据集的公平性时需要调查的一些重要问题：\n",
    "\n",
    "* **大量观察数据中是否有缺失的特征值？**\n",
    "* **缺失的特征是否有可能影响其他特征？**\n",
    "* **是否存在意外特征值？**\n",
    "* **您看到了哪些数据偏斜迹象？**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "9yCIuAqWA1Pm"
   },
   "source": [
    "首先，我们可以使用 [Facets Overview](https://pair-code.github.io/facets/)，这是一个交互式可视化工具，可以帮助我们探索数据集。通过 Facets Overview，我们可以快速分析成年人数据集中各个值的分布情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "MW-qryqs1gig"
   },
   "outputs": [],
   "source": [
    "#@title Visualize the Data in Facets\n",
    "fsg = FeatureStatisticsGenerator()\n",
    "dataframes = [\n",
    "    {'table': train_df, 'name': 'trainData'}]\n",
    "censusProto = fsg.ProtoFromDataFrames(dataframes)\n",
    "protostr = base64.b64encode(censusProto.SerializeToString()).decode(\"utf-8\")\n",
    "\n",
    "\n",
    "HTML_TEMPLATE = \"\"\"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.3.3/webcomponents-lite.js\"></script>\n",
    "        <link rel=\"import\" href=\"https://raw.githubusercontent.com/PAIR-code/facets/1.0.0/facets-dist/facets-jupyter.html\">\n",
    "        <facets-overview id=\"elem\"></facets-overview>\n",
    "        <script>\n",
    "          document.querySelector(\"#elem\").protoInput = \"{protostr}\";\n",
    "        </script>\"\"\"\n",
    "html = HTML_TEMPLATE.format(protostr=protostr)\n",
    "display(HTML(html))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "91wjnZFpPWw-"
   },
   "source": [
    "### 公平意识任务 1\n",
    "\n",
    "查看每个数值和连续特征的描述性统计信息和直方图。点击类别特征直方图上方的 **Show Raw Data** 按钮，查看每个类别的值分布。\n",
    "\n",
    "然后，试着回答前面的问题：\n",
    "\n",
    "1. 大量观察数据中是否有缺失的特征值？\n",
    "2. 缺失的特征是否有可能影响其他特征？\n",
    "3. 是否存在意外特征值？\n",
    "4. 您看到了哪些数据偏斜迹象？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "KlF-lQ8yQ69b"
   },
   "source": [
    "### 解决方案\n",
    "\n",
    "点击下方即可查看我们发现的一些数据分析结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "xX_qjj5AQ_Hb"
   },
   "source": [
    "查看数值特征和类别特征的**缺失**列后，可以看到没有缺失的特征值，所以这不是一个问题。\n",
    "\n",
    "通过观察每个数值特征的最小/最大值和直方图，我们可以确定数据集中的任何极端离群值。对于 `hours_per_week`，我们可以看到最小值是 1，这可能有点奇怪，因为大部分工作都要求每周工作多个小时。对于 `capital_gain` 和 `capital_loss`，我们可以看到超过 90% 的值是 0。考虑到只有进行投资的个体才会登记资本收益/损失，那么，该特征有不足 10% 的样本具有非零值当然是合理的，但是，我们可能需要更仔细地验证这些特征值是否有效。\n",
    "\n",
    "通过观察性别直方图，我们发现超过三分之二（约 67%）的样本代表男性。这有力地表明了数据偏斜现象，因为我们期望两个性别之间的划分接近 50/50。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "hKj2hz-Sql7V"
   },
   "source": [
    "### 深入分析\n",
    "\n",
    "为了进一步探索数据集，我们可以使用 [Facets Dive](https://pair-code.github.io/facets/)，这个工具提供了一个交互界面，在此界面中，可视化图表中的每个项目都代表一个数据点。但是要使用 Facets Dive，我们需要将数据转换为 JSON 数组。\n",
    "幸好，DataFrame 方法 `to_json()` 为我们解决了这个问题。\n",
    "\n",
    "请运行下面的单元格，以将数据转换为 JSON 数组并加载 Facets Dive。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "stlklrG_xssF"
   },
   "outputs": [],
   "source": [
    "#@title Set the Number of Data Points to Visualize in Facets Dive\n",
    "\n",
    "SAMPLE_SIZE = 2500 #@param\n",
    "  \n",
    "train_dive = train_df.sample(SAMPLE_SIZE).to_json(orient='records')\n",
    "\n",
    "HTML_TEMPLATE = \"\"\"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.3.3/webcomponents-lite.js\"></script>\n",
    "        <link rel=\"import\" href=\"https://raw.githubusercontent.com/PAIR-code/facets/1.0.0/facets-dist/facets-jupyter.html\">\n",
    "        <facets-dive id=\"elem\" height=\"600\"></facets-dive>\n",
    "        <script>\n",
    "          var data = {jsonstr};\n",
    "          document.querySelector(\"#elem\").data = data;\n",
    "        </script>\"\"\"\n",
    "html = HTML_TEMPLATE.format(jsonstr=train_dive)\n",
    "display(HTML(html))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "LxqAPDcRDFB2"
   },
   "source": [
    "## 公平意识任务 2\n",
    "\n",
    "使用可视化图表左侧面板上的菜单更改数据的组织方式：\n",
    "\n",
    "1. 在 **Faceting | X-Axis** 菜单中，选择 **education**，在 **Display | Color** 和 **Display | Type** 菜单中，选择 **income_bracket**。您会如何描述教育水平和收入档次之间的关系？\n",
    "\n",
    "2. 接下来，在 **Faceting | X-Axis** 菜单中，选择 **marital_status**，在 **Display | Color** 和 **Display | Type** 菜单中，选择 **gender**。对于每个 marital-status 类别的性别分布，您能观察到哪些值得注意的现象？\n",
    "\n",
    "在执行上述任务时，请记住以下与公平性有关的问题：\n",
    "\n",
    "* **缺失了什么？**\n",
    "* **什么被过度概括了？**\n",
    "* **什么没有代表性？**\n",
    "* **变量及其值如何反映真实世界？**\n",
    "* **我们可能遗漏了什么？**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "qZ-9vJgSEpHj"
   },
   "source": [
    "### 解决方案\n",
    "\n",
    "点击下方即可查看我们发现的一些数据分析结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "uYpbgdATEx8L"
   },
   "source": [
    "1. 在我们的数据集中，较高的教育水平通常与较高的收入档次相关联。在教育水平为本科或以上的样本中，收入水平超过 5 万美元的人所占比例更大。\n",
    "\n",
    "2. 在大多数 marital-status 类别中，男性和女性值的分布接近 1:1。唯一值得注意的例外情况是“married-civ-spouse”，这时候男性超出了女性，男女比例超过 5:1。考虑到我们在任务 1 中已经发现在数据集中男性的比例特别高，那么我们现在可以推断已婚女性在数据中所占比例特别低。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "7YVH8hYfSjer"
   },
   "source": [
    "### 摘要\n",
    "\n",
    "绘制直方图、从最普遍到最不普遍样本进行排列、确定重复或缺失的样本、确保训练和测试集类似、计算特征分位数 - **所有这些都是对数据的重要分析。**\n",
    "\n",
    "**您对数据状况越了解，就越有可能知道哪些方面会受到不公平性的影响！**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "2ivWw9Wpj67m"
   },
   "source": [
    "### 公平意识任务 3\n",
    "\n",
    "在使用 Facets 探索数据集后，看看您能否根据所了解到的特征信息，确定可能出现的公平性方面的问题。\n",
    "\n",
    "以下哪些特征可能会带来公平性方面的问题？\n",
    "\n",
    "请从以下单元格中的下拉选项中选择一种特征，然后运行该单元格以检查您的答案。接下来，浏览其余选项，以便更深入地了解每个选项对模型的预测有何影响。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "8bFDVCV1sxiX"
   },
   "outputs": [],
   "source": [
    "feature = 'capital_gain / capital_loss' #@param [\"\", \"hours_per_week\", \"fnlwgt\", \"gender\", \"capital_gain / capital_loss\", \"age\"] {allow-input: false}\n",
    "\n",
    "\n",
    "if feature == \"hours_per_week\":\n",
    "  print(\n",
    "'''It does seem a little strange to see 'hours_per_week' max out at 99 hours,\n",
    "which could lead to data misrepresentation. One way to address this is by\n",
    "representing 'hours_per_week' as a binary \"working 40 hours/not working 40\n",
    "hours\" feature. Also keep in mind that data was extracted based on work hours\n",
    "being greater than 0. In other words, this feature representation exclude a\n",
    "subpopulation of the US that is not working. This could skew the outcomes of the\n",
    "model.''')\n",
    "if feature == \"fnlwgt\":\n",
    "  print(\n",
    "\"\"\"'fnlwgt' represents the weight of the observations. After fitting the model\n",
    "to this data set, if certain group of individuals end up performing poorly \n",
    "compared to other groups, then we could explore ways of reweighting each data \n",
    "point using this feature.\"\"\")\n",
    "if feature == \"gender\":\n",
    "  print(\n",
    "\"\"\"Looking at the ratio between men and women shows how disproportionate the data\n",
    "is compared to the real world where the ratio (at least in the US) is closer to\n",
    "1:1. This could pose a huge probem in performance across gender. Considerable\n",
    "measures may need to be taken to upsample the underrepresented group (in this\n",
    "case, women).\"\"\")\n",
    "if feature == \"capital_gain / capital_loss\":\n",
    "  print(\n",
    "\"\"\"Both 'capital_gain' and 'capital_loss' have very low variance, which might\n",
    "suggest they don't contribute a whole lot of information for predicting income. It\n",
    "may be okay to omit these features rather than giving the model more noise.\"\"\")\n",
    "if feature == \"age\":\n",
    "  print(\n",
    "'''\"age\" has a lot of variance, so it might benefit from bucketing to learn\n",
    "fine-grained correlations between income and age, as well as to prevent\n",
    "overfitting.''')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "n3OT-YVpftEI"
   },
   "source": [
    "## 使用 TensorFlow Estimator 进行预测\n",
    "\n",
    "我们已经对成年人数据集有了更好的了解，下面我们就可以开始创建神经网络来预测收入了。在此部分，我们将使用 TensorFlow 的 Estimator API 访问 `DNNClassifier` 类"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ECBRATBVG4rn"
   },
   "source": [
    "### 将成年人数据集转换为张量\n",
    "首先我们必须定义输入函数，此函数将采用位于 Pandas DataFrame 中的成年人数据集，并使用 ```tf.estimator.inputs.pandas_input_fn()``` 函数将该数据集转换为张量。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "Bt-rQvJLx4Hm"
   },
   "outputs": [],
   "source": [
    "def csv_to_pandas_input_fn(data, batch_size=100, num_epochs=1, shuffle=False):\n",
    "  return tf.estimator.inputs.pandas_input_fn(\n",
    "      x=data.drop('income_bracket', axis=1),\n",
    "      y=data['income_bracket'].apply(lambda x: \">50K\" in x).astype(int),\n",
    "      batch_size=batch_size,\n",
    "      num_epochs=num_epochs,\n",
    "      shuffle=shuffle,\n",
    "      num_threads=1)\n",
    "\n",
    "print('csv_to_pandas_input_fn() defined.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "0mz2sts6IjBO"
   },
   "source": [
    "### 在 TensorFlow 中表示特征\n",
    "TensorFlow 要求将数据映射到模型。为此，您必须使用 ```tf.feature_columns``` 在 TensorFlow 中获取并表示特征。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "tAG5hUJwx725"
   },
   "outputs": [],
   "source": [
    "#@title Categorical Feature Columns\n",
    "\n",
    "# Since we don't know the full range of possible values with occupation and\n",
    "# native_country, we'll use categorical_column_with_hash_bucket() to help map\n",
    "# each feature string into an integer ID.\n",
    "occupation = tf.feature_column.categorical_column_with_hash_bucket(\n",
    "    \"occupation\", hash_bucket_size=1000)\n",
    "native_country = tf.feature_column.categorical_column_with_hash_bucket(\n",
    "    \"native_country\", hash_bucket_size=1000)\n",
    "\n",
    "# For the remaining categorical features, since we know what the possible values\n",
    "# are, we can be more explicit and use categorical_column_with_vocabulary_list()\n",
    "gender = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"gender\", [\"Female\", \"Male\"])\n",
    "race = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"race\", [\n",
    "        \"White\", \"Asian-Pac-Islander\", \"Amer-Indian-Eskimo\", \"Other\", \"Black\"\n",
    "    ])\n",
    "education = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"education\", [\n",
    "        \"Bachelors\", \"HS-grad\", \"11th\", \"Masters\", \"9th\",\n",
    "        \"Some-college\", \"Assoc-acdm\", \"Assoc-voc\", \"7th-8th\",\n",
    "        \"Doctorate\", \"Prof-school\", \"5th-6th\", \"10th\", \"1st-4th\",\n",
    "        \"Preschool\", \"12th\"\n",
    "    ])\n",
    "marital_status = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"marital_status\", [\n",
    "        \"Married-civ-spouse\", \"Divorced\", \"Married-spouse-absent\",\n",
    "        \"Never-married\", \"Separated\", \"Married-AF-spouse\", \"Widowed\"\n",
    "    ])\n",
    "relationship = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"relationship\", [\n",
    "        \"Husband\", \"Not-in-family\", \"Wife\", \"Own-child\", \"Unmarried\",\n",
    "        \"Other-relative\"\n",
    "    ])\n",
    "workclass = tf.feature_column.categorical_column_with_vocabulary_list(\n",
    "    \"workclass\", [\n",
    "        \"Self-emp-not-inc\", \"Private\", \"State-gov\", \"Federal-gov\",\n",
    "        \"Local-gov\", \"?\", \"Self-emp-inc\", \"Without-pay\", \"Never-worked\"\n",
    "    ])\n",
    "\n",
    "print('Categorical feature columns defined.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "Jwtuu8MmyKCJ"
   },
   "outputs": [],
   "source": [
    "#@title Numeric Feature Columns\n",
    "# For Numeric features, we can just call on feature_column.numeric_column()\n",
    "# to use its raw value instead of having to create a map between value and ID.\n",
    "age = tf.feature_column.numeric_column(\"age\")\n",
    "fnlwgt = tf.feature_column.numeric_column(\"fnlwgt\")\n",
    "education_num = tf.feature_column.numeric_column(\"education_num\")\n",
    "capital_gain = tf.feature_column.numeric_column(\"capital_gain\")\n",
    "capital_loss = tf.feature_column.numeric_column(\"capital_loss\")\n",
    "hours_per_week = tf.feature_column.numeric_column(\"hours_per_week\")\n",
    "\n",
    "print('Numeric feature columns defined.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "3WqAbug6jePb"
   },
   "source": [
    "#### 将年龄变成类别特征\n",
    "\n",
    "如果您在完成**公平意识任务 3** 时选择了 `age`，则会注意到我们建议对 `age` 进行*分桶*（也称为*分箱*）可能效果更好，即将年龄相近的人员分成一组。这可能有助于模型在各年龄段中更好地泛化。因此，我们将 `age` 从数值特征（从技术上讲是[排序特征](https://en.wikipedia.org/wiki/Ordinal_data)）转换为类别特征。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "HxVm8X15yLR7"
   },
   "outputs": [],
   "source": [
    "age_buckets = tf.feature_column.bucketized_column(\n",
    "    age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "2lx4JuLdi7jw"
   },
   "source": [
    "#### 考虑关键子群组\n",
    "\n",
    "在执行特征工程时，请务必记住您使用的数据可能来自属于不同子群组的个人，您需要针对这些子群组分别评估模型的表现。\n",
    "\n",
    "**_注意：_***在这种情况下，子群组定义为一组同时具有指定特征（例如种族、性别或性取向）的个人，在围绕公平性评估模型时要特别考虑这些子群组。*\n",
    "\n",
    "当我们希望模型缓解或利用从与某个子群组相关的特征了解到的信号时，我们需要使用不同类型的工具和技术（**其中大多数工具和技术在此时间点仍处于开放研究阶段**）。\n",
    "\n",
    "在使用不同变量并为其定义任务时，思考接下来该做什么可能会有帮助。例如，*变量和任务的交互什么情况下可能存在问题？*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "5aD1OM8egad9"
   },
   "source": [
    "### 定义模型特征\n",
    "\n",
    "现在，我们可以明确定义在模型中添加哪个特征。\n",
    "\n",
    "我们将为 `gender` 设定子群组，并将其保存在单独的 `subgroup_variables` 列表中，以便我们可以根据需要为其添加特殊处理。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "O68xV_24gbnD"
   },
   "outputs": [],
   "source": [
    "# List of variables, with special handling for gender subgroup.\n",
    "variables = [native_country, education, occupation, workclass,\n",
    "             relationship, age_buckets]\n",
    "subgroup_variables = [gender]\n",
    "feature_columns = variables + subgroup_variables"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "3nYSMg67jWaA"
   },
   "source": [
    "### 用成年人数据集训练深度神经网络模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "_kRL5rScH1F7"
   },
   "source": [
    "准备好特征后，我们就可以尝试使用深度学习预测收入了。\n",
    "\n",
    "为了简单起见，我们会使神经网络架构很精简，直接**定义一个包含两个隐藏层的前馈神经网络**。\n",
    "\n",
    "但首先，我们必须将高维度类别特征转换为低维度的密集实值向量（称之为嵌入向量）。幸好，```indicator_column```（将其视为独热编码）和 ```embedding_column```（可将稀疏特征转换为密集特征）可帮助我们简化此流程。\n",
    "\n",
    "以下单元格创建了进一步定义模型所需的深度列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "code",
    "colab": {},
    "colab_type": "code",
    "id": "bnyw4cyLTSUB"
   },
   "outputs": [],
   "source": [
    "deep_columns = [\n",
    "    tf.feature_column.indicator_column(workclass),\n",
    "    tf.feature_column.indicator_column(education),\n",
    "    tf.feature_column.indicator_column(age_buckets),\n",
    "    tf.feature_column.indicator_column(gender),\n",
    "    tf.feature_column.indicator_column(relationship),\n",
    "    tf.feature_column.embedding_column(native_country, dimension=8),\n",
    "    tf.feature_column.embedding_column(occupation, dimension=8),\n",
    "]\n",
    "\n",
    "print(deep_columns)\n",
    "print('Deep columns created.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "lBaCn_Z1PshC"
   },
   "source": [
    "完成所有数据预处理工作后，我们现在可以定义深度神经网络模型了。首先使用下面定义的参数。（稍后，在您定义评估指标并评估模型后，您可以返回并调整这些参数以比较结果。）\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "tQZ1kumWk8XO"
   },
   "outputs": [],
   "source": [
    "#@title Define Deep Neural Net Model\n",
    "\n",
    "HIDDEN_UNITS = [1024, 512] #@param\n",
    "LEARNING_RATE = 0.1 #@param\n",
    "L1_REGULARIZATION_STRENGTH = 0.0001 #@param\n",
    "L2_REGULARIZATION_STRENGTH = 0.0001 #@param\n",
    "\n",
    "model_dir = tempfile.mkdtemp()\n",
    "single_task_deep_model = tf.estimator.DNNClassifier(\n",
    "    feature_columns=deep_columns,\n",
    "    hidden_units=HIDDEN_UNITS,\n",
    "    optimizer=tf.train.ProximalAdagradOptimizer(\n",
    "      learning_rate=LEARNING_RATE,\n",
    "      l1_regularization_strength=L1_REGULARIZATION_STRENGTH,\n",
    "      l2_regularization_strength=L2_REGULARIZATION_STRENGTH),\n",
    "    model_dir=model_dir)\n",
    "\n",
    "print('Deep neural net model defined.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "Tjhqo9XOP2VV"
   },
   "source": [
    "为了简单起见，我们将训练 1000 步，但您可以随意尝试调整此参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "UtrhAXwvqtVD"
   },
   "outputs": [],
   "source": [
    "#@title Fit Deep Neural Net Model to the Adult Training Dataset\n",
    "\n",
    "STEPS = 1000 #@param\n",
    "\n",
    "single_task_deep_model.train(\n",
    "    input_fn=csv_to_pandas_input_fn(train_df, num_epochs=None, shuffle=True),\n",
    "    steps=STEPS);\n",
    "\n",
    "print('Deep neural net model is done fitting.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "m0UHu5t-P7G7"
   },
   "source": [
    "我们现在可以使用预留测试集评估模型的整体表现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "HDV8hYqvncCy"
   },
   "outputs": [],
   "source": [
    "#@title Evaluate Deep Neural Net Performance\n",
    "\n",
    "results = single_task_deep_model.evaluate(\n",
    "    input_fn=csv_to_pandas_input_fn(test_df, num_epochs=1, shuffle=False),\n",
    "    steps=None)\n",
    "print(\"model directory = %s\" % model_dir)\n",
    "print(\"---- Results ----\")\n",
    "for key in sorted(results):\n",
    "  print(\"%s: %s\" % (key, results[key]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "7j0LrXMGlTDl"
   },
   "source": [
    "您可以尝试使用不同的参数重新训练模型。最后，您会发现深度神经网络在预测收入方面表现不错。\n",
    "\n",
    "**但是缺少与子群组相关的评估指标。**我们将在下一部分介绍您可以在子群组级别进行评估的一些方法。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "sbwmbnUUU1kY"
   },
   "source": [
    "## 使用混淆矩阵评估公平性\n",
    "\n",
    "虽然评估模型的总体效果可让我们了解该模型的质量，但无法让我们充分了解模型针对不同子群组的表现如何。\n",
    "\n",
    "在评估模型的公平性时，请务必确定预测错误在所有子群组中是否保持统一，或者某些子群组是否比其他子群组更容易出现特定预测错误。\n",
    "\n",
    "*混淆矩阵*是一种用于比较不同种类模型错误的发生率的重要工具。记得在[机器学习速成课程的分类单元](https://developers.google.com/machine-learning/crash-course/classification/true-false-positive-negative)中，我们提到：混淆矩阵是一个网格，它可以绘制模型的预测值和真实值，并将统计信息制成表格，总结了模型做出正确预测和错误预测的频率。\n",
    "\n",
    "我们首先为收入预测模型创建二元混淆矩阵，之所以为二元，是因为我们的标签 (`income_bracket`) 只包含两个可能的值（`<50K` 或 `>50K`）。我们将收入 `>50K` 定义为**正标签**，并将收入 `<50k` 定义为**负标签**。\n",
    "\n",
    "**注意：**在这种情况下，*正*和*负*不应被解释为价值评判（我们并非认为年收入高于 5 万的人比年收入低于 5 万的人更优秀）。它们只是用于区分模型可做出的两种可能预测的标准术语。\n",
    "\n",
    "模型做出正确预测（预测结果与真实值相符）的情况被归类为 **true**，模型做出错误预测的情况被归类为 **false**。\n",
    "\n",
    "因此，我们的混淆矩阵会列出四个可能的状态：\n",
    "\n",
    "* **真正例**：模型预测结果为 `>50K`，并且此结果是真实值。\n",
    "* **真负例**：模型预测结果为 `<50K`，并且此结果是真实值。\n",
    "* **假正例**：模型预测结果为 `>50K`，并且此结果与真实情况冲突。\n",
    "* **假负例**：模型预测结果为 `<50K`，并且此结果与真实情况冲突。\n",
    "\n",
    "**注意：**如果需要，我们可以使用每个状态的结果计算辅助评估指标，例如[精确率和召回率](https://developers.google.com/machine-learning/crash-course/classification/precision-and-recall)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "nsUj_XZHU_mI"
   },
   "source": [
    "### 绘制混淆矩阵\n",
    "\n",
    "以下单元格定义了一个函数，该函数使用 `sklearn.metrics.confusion_matrix` 模块计算算出二元混淆矩阵和评估指标所需的所有实例（真正例、真负例、假正例、假负例）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "CmOWym53VITS"
   },
   "outputs": [],
   "source": [
    "#@test {\"output\": \"ignore\"}\n",
    "#@title Define Function to Compute Binary Confusion Matrix Evaluation Metrics\n",
    "def compute_eval_metrics(references, predictions):\n",
    "  tn, fp, fn, tp = confusion_matrix(references, predictions).ravel()\n",
    "  precision = tp / float(tp + fp)\n",
    "  recall = tp / float(tp + fn)\n",
    "  false_positive_rate = fp / float(fp + tn)\n",
    "  false_omission_rate = fn / float(tn + fn)\n",
    "  return precision, recall, false_positive_rate, false_omission_rate\n",
    "\n",
    "print('Binary confusion matrix and evaluation metrics defined.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ooYe_3lqTCPz"
   },
   "source": [
    "我们还需要绘制二元混淆矩阵方面的帮助。以下函数结合使用各种第三方模块（Pandas DataFrame、Matplotlib、Seaborn）来绘制混淆矩阵。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "TMIN_nl1VFuS"
   },
   "outputs": [],
   "source": [
    "#@title Define Function to Visualize Binary Confusion Matrix\n",
    "def plot_confusion_matrix(confusion_matrix, class_names, figsize = (8,6)):\n",
    "    # We're taking our calculated binary confusion matrix that's already in form \n",
    "    # of an array and turning it into a Pandas DataFrame because it's a lot \n",
    "    # easier to work with when visualizing a heat map in Seaborn.\n",
    "    df_cm = pd.DataFrame(\n",
    "        confusion_matrix, index=class_names, columns=class_names, \n",
    "    )\n",
    "    fig = plt.figure(figsize=figsize)\n",
    "    \n",
    "    # Combine the instance (numercial value) with its description\n",
    "    strings = np.asarray([['True Positives', 'False Negatives'],\n",
    "                          ['False Positives', 'True Negatives']])\n",
    "    labels = (np.asarray(\n",
    "        [\"{0:d}\\n{1}\".format(value, string) for string, value in zip(\n",
    "            strings.flatten(), confusion_matrix.flatten())])).reshape(2, 2)\n",
    "\n",
    "    heatmap = sns.heatmap(df_cm, annot=labels, fmt=\"\");\n",
    "    heatmap.yaxis.set_ticklabels(\n",
    "        heatmap.yaxis.get_ticklabels(), rotation=0, ha='right')\n",
    "    heatmap.xaxis.set_ticklabels(\n",
    "        heatmap.xaxis.get_ticklabels(), rotation=45, ha='right')\n",
    "    plt.ylabel('References')\n",
    "    plt.xlabel('Predictions')\n",
    "    return fig\n",
    "\n",
    "print('Binary confusion matrix visualization defined.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "hUvBYtwXVzlQ"
   },
   "source": [
    "现在我们已经定义了所有必要的函数，接下来可以使用[我们的深度神经网络模型](#scrollTo=3nYSMg67jWaA)的结果计算二元混淆矩阵和评估指标。此单元格的输出是一个带标签视图，我们可以通过这一视图在混淆矩阵和评估指标表格之间切换。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "9enf_Jfi-AVS"
   },
   "source": [
    "### 公平意识任务 4\n",
    "\n",
    "使用以下表单为两个性别子群组（`Female` 和 `Male`）生成混淆矩阵。对每个子群组的假正例和假负例数量进行比较。错误率是否存在显著差异，表明模型对于一个子群组的表现要比另一个子群组好？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "cellView": "form",
    "colab": {},
    "colab_type": "code",
    "id": "5TBzaWs1VKTa"
   },
   "outputs": [],
   "source": [
    "#@title Visualize Binary Confusion Matrix and Compute Evaluation Metrics Per Subgroup\n",
    "CATEGORY  =  \"gender\" #@param {type:\"string\"}\n",
    "SUBGROUP =  \"Male\" #@param {type:\"string\"}\n",
    "\n",
    "# Given define subgroup, generate predictions and obtain its corresponding \n",
    "# ground truth.\n",
    "predictions_dict = single_task_deep_model.predict(input_fn=csv_to_pandas_input_fn(\n",
    "    test_df.loc[test_df[CATEGORY] == SUBGROUP], num_epochs=1, shuffle=False))\n",
    "predictions = []\n",
    "for prediction_item, in zip(predictions_dict):\n",
    "    predictions.append(prediction_item['class_ids'][0])\n",
    "actuals = list(\n",
    "    test_df.loc[test_df[CATEGORY] == SUBGROUP]['income_bracket'].apply(\n",
    "        lambda x: '>50K' in x).astype(int))\n",
    "classes = ['Over $50K', 'Less than $50K']\n",
    "\n",
    "# To stay consistent, we have to flip the confusion \n",
    "# matrix around on both axes because sklearn's confusion matrix module by\n",
    "# default is rotated.\n",
    "rotated_confusion_matrix = np.fliplr(confusion_matrix(actuals, predictions))\n",
    "rotated_confusion_matrix = np.flipud(rotated_confusion_matrix)\n",
    "\n",
    "tb = widgets.TabBar(['Confusion Matrix', 'Evaluation Metrics'], location='top')\n",
    "\n",
    "with tb.output_to('Confusion Matrix'):\n",
    "  plot_confusion_matrix(rotated_confusion_matrix, classes);\n",
    "\n",
    "with tb.output_to('Evaluation Metrics'):\n",
    "  grid = widgets.Grid(2,4)\n",
    "\n",
    "  p, r, fpr, fomr = compute_eval_metrics(actuals, predictions)\n",
    "\n",
    "  with grid.output_to(0, 0):\n",
    "    print(' Precision ')\n",
    "  with grid.output_to(1, 0):\n",
    "    print(' %.4f ' % p)\n",
    "\n",
    "  with grid.output_to(0, 1):\n",
    "    print(' Recall ')\n",
    "  with grid.output_to(1, 1):\n",
    "    print(' %.4f ' % r)\n",
    "\n",
    "  with grid.output_to(0, 2):\n",
    "    print(' False Positive Rate ')\n",
    "  with grid.output_to(1, 2):\n",
    "    print(' %.4f ' % fpr)\n",
    "\n",
    "  with grid.output_to(0, 3):\n",
    "    print(' False Omission Rate ')\n",
    "  with grid.output_to(1, 3):\n",
    "    print(' %.4f ' % fomr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "TF3B5h3c-7Fb"
   },
   "source": [
    "### 解决方案\n",
    "\n",
    "点击下方即可查看我们发现的一些数据分析结果"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "dhKR49AT_5ZK"
   },
   "source": [
    "使用默认的模型参数，您可能会发现模型对于男性的表现要比女性好。具体而言，在运行模型后，我们发现男性的精确率和召回率（分别为 0.7490 和 0.4795）都比女性（分别为 0.6787 和 0.3716）高。\n",
    "\n",
    "希望通过此混淆矩阵演示，您可以发现结果与整体效果指标略有不同，并强调了评估模型在各子群组中的表现（而不是总体表现）的重要性。\n",
    "\n",
    "在您的工作中，确保您能在权衡假正例、假负例、真正例和真负例方面做出明智的决策。例如，您可能需要实现非常低的假正例率和较高的真正例率，或者可能需要实现较高的精确率，而较低的召回率能接受。\n",
    "\n",
    "**请根据这些权衡需求选择评估指标。**"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "collapsed_sections": [
    "KlF-lQ8yQ69b",
    "qZ-9vJgSEpHj",
    "TF3B5h3c-7Fb"
   ],
   "name": "intro_to_fairness.ipynb",
   "provenance": [],
   "version": "0.3.2"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
